feat: claude agent teams UI support#1145
feat: claude agent teams UI support#1145JustYannicc wants to merge 11 commits intopingdotgg:mainfrom
Conversation
Status tracking:
- Add tool.completed → idle mapping so agents transition correctly between tools
- Fix status fallback to preserve current status instead of forcing "running"
- Guard terminal status from regression by late tool.completed events
- Compute endedAt before status in finalization so ended runs show correct state
- Track teammate taskIds across task.progress/task.completed events
- Fall back to ended runs by runId for late-arriving events
Shutdown detection:
- Handle team.run.ended in main loop before shouldTrackAgentTeamsActivity filter
- Treat TeamDelete tool.completed as team shutdown signal
- Force non-terminal members to completed when run ends
- Auto-close stale synthetic turns in sendTurn to prevent session deadlock
Lead tool filtering:
- TeamCreate/TeamDelete/TeamUpdate/SendMessage/TaskCreate/TaskUpdate/TaskDelete
no longer create phantom team members
- Route SendMessage activities to target member's activity feed
- Classify team management tools as collab_agent_tool_call in adapter
Adapter improvements:
- Accumulate input_json_delta fragments for tool input reconstruction
- Fix SendMessage field names (recipient/content per SDK schema)
- Extract teammateName from toolInput.recipient for SendMessage tools
- Auto-start synthetic turns for assistant messages arriving without turnState
(fixes invisible teammate responses between user prompts)
- Clear inFlightTools between turns to prevent stale fragment corruption
- Treat "auto" teammateMode as undefined so fallback to in-process fires
Ingestion:
- Recognize taskType "in_process_teammate" as team metadata
- Extract teammate names from detail prefix ("name: description...")
- Infer teammateName for task.started/progress/completed payloads
UI panel redesign:
- Replace redundant card grid + sidebar with compact master-detail layout
- Left column: clickable agent rows with name, status, current tool
- Right column: identity header + unified activity feed (activities + tasks)
- Fix COLOR_NAME_MAP for direct color name lookup (red→rose, blue→sky, etc.)
- Fix panelSummary double-counting idle in active count
- Add formatRelativeTime and buildUnifiedFeed helpers
- Max-height scroll constraints on panel and activity feed
- Panel detaches from top when team ends (shows in timeline instead)
Settings:
- Add claudeTeammateMode setting (auto/in-process/tmux) with UI dropdown
- Forward teammateMode through provider options to adapter
- Add teammateMode to ClaudeCodeProviderStartOptions contract
Timeline:
- Enriched team-run entries with memberCount, summary, duration badge
- Styled team-run cards with icon and elapsed time
Tests:
- 15 new unit tests covering full team lifecycle, phantom member prevention,
status transitions, TeamDelete shutdown, task.progress tracking, SendMessage
routing, multiple runs, and terminal status regression
- 3 E2E tests using real Claude API (Haiku 4.5): team lifecycle, subagent
result flow, background command capture
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment Tip You can disable sequence diagrams in the walkthrough.Disable the |
|
I am still working on the UI. Will update with screenshots. But filed for now since I was asked to split it out in #179 |
| stopped: false, | ||
| }; | ||
| yield* Ref.set(contextRef, context); | ||
| sessions.set(threadId, context); |
There was a problem hiding this comment.
🟡 Medium Layers/ClaudeCodeAdapter.ts:2174
sessions.set(threadId, context) at line 2174 silently overwrites any existing session with the same threadId. The previous session's resources—the forked runSdkStream fiber, promptQueue, and queryRuntime—become orphaned and continue running indefinitely, and the finalizer only cleans up sessions still present in the sessions map. Consider either rejecting duplicate threadIds with an error, or explicitly stopping the existing session before overwriting it.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/provider/Layers/ClaudeCodeAdapter.ts around line 2174:
`sessions.set(threadId, context)` at line 2174 silently overwrites any existing session with the same `threadId`. The previous session's resources—the forked `runSdkStream` fiber, `promptQueue`, and `queryRuntime`—become orphaned and continue running indefinitely, and the finalizer only cleans up sessions still present in the `sessions` map. Consider either rejecting duplicate `threadId`s with an error, or explicitly stopping the existing session before overwriting it.
Evidence trail:
apps/server/src/provider/Layers/ClaudeCodeAdapter.ts line 864 (sessions map declaration), lines 1920-1924 (startSession using threadId without checking existing sessions), line 2174 (`sessions.set(threadId, context)` without has() check), line 2228 (`Effect.runFork(runSdkStream(context))` forking detached fiber), line 1884 (`sessions.delete(context.session.threadId)` cleanup only for sessions in map). git_grep for `sessions.has` returned no results, confirming no duplicate check exists.
| function formatRelativeTime(iso: string): string { | ||
| const diff = Date.now() - new Date(iso).getTime(); | ||
| const minutes = Math.floor(diff / 60_000); | ||
| if (minutes < 1) return "just now"; | ||
| if (minutes < 60) return `${minutes}m ago`; | ||
| const hours = Math.floor(minutes / 60); | ||
| if (hours < 24) return `${hours}h ago`; | ||
| return `${Math.floor(hours / 24)}d ago`; | ||
| } |
There was a problem hiding this comment.
🟢 Low chat/AgentTeamsPanel.tsx:174
When formatRelativeTime receives an invalid date string (e.g., empty string or malformed), new Date(iso).getTime() returns NaN. Since NaN < 1, NaN < 60, and NaN < 24 are all false, the function falls through to return "NaNd ago". Consider adding a Number.isNaN guard to match the pattern used in formatTimestamp.
function formatRelativeTime(iso: string): string {
+ const time = new Date(iso).getTime();
+ if (Number.isNaN(time)) return iso;
+ const diff = Date.now() - time;
- const diff = Date.now() - new Date(iso).getTime();
const minutes = Math.floor(diff / 60_000);🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/web/src/components/chat/AgentTeamsPanel.tsx around lines 174-182:
When `formatRelativeTime` receives an invalid date string (e.g., empty string or malformed), `new Date(iso).getTime()` returns `NaN`. Since `NaN < 1`, `NaN < 60`, and `NaN < 24` are all `false`, the function falls through to return `"NaNd ago"`. Consider adding a `Number.isNaN` guard to match the pattern used in `formatTimestamp`.
Evidence trail:
apps/web/src/components/chat/AgentTeamsPanel.tsx lines 160-181 at REVIEWED_COMMIT: `formatTimestamp` (lines 160-171) has `Number.isNaN` guard at line 162-164, while `formatRelativeTime` (lines 173-181) lacks this guard. JavaScript specification: NaN comparisons with any number return false, causing the function to fall through all conditionals.
| const awaitingLeaderApproval = | ||
| asBoolean(value?.awaitingLeaderApproval) ?? asBoolean(value?.awaiting_leader_approval); |
There was a problem hiding this comment.
🟢 Low Layers/ClaudeCodeAdapter.ts:280
The property access order for awaitingLeaderApproval at line 280 is inverted: it checks camelCase first (value?.awaitingLeaderApproval) then falls back to snake_case, while every other field checks snake_case first. If input contains both naming conventions with different values, this field resolves to a different value than all others.
- const awaitingLeaderApproval =
- asBoolean(value?.awaitingLeaderApproval) ?? asBoolean(value?.awaiting_leader_approval);
+ const awaitingLeaderApproval =
+ asBoolean(value?.awaiting_leader_approval) ?? asBoolean(value?.awaitingLeaderApproval);🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/provider/Layers/ClaudeCodeAdapter.ts around lines 280-281:
The property access order for `awaitingLeaderApproval` at line 280 is inverted: it checks `camelCase` first (`value?.awaitingLeaderApproval`) then falls back to `snake_case`, while every other field checks `snake_case` first. If input contains both naming conventions with different values, this field resolves to a different value than all others.
Evidence trail:
apps/server/src/provider/Layers/ClaudeCodeAdapter.ts lines 270-281 at REVIEWED_COMMIT showing:
- Lines 270-279: All other fields check snake_case first, then camelCase (e.g., `value?.team_name ?? value?.teamName`)
- Lines 280-281: `awaitingLeaderApproval` checks camelCase first, then snake_case (`value?.awaitingLeaderApproval ?? value?.awaiting_leader_approval`)
| if (teamKey) { | ||
| byTeamKey.set(teamKey, snapshot); | ||
| } |
There was a problem hiding this comment.
🟢 Low src/session-logic.ts:943
When a runId's teamKey changes from "A" to "B" across activities, line 944 only sets byTeamKey.set("B", snapshot) without removing the stale entry for "A". The old byTeamKey.get("A") still returns the outdated snapshot, leaving the index in an inconsistent state where both keys reference different versions of the same run. Consider deleting the old teamKey entry when it differs from the current one, or rebuild the index differently.
- if (teamKey) {
+ if (teamKey && teamKey !== existing?.teamKey) {
+ if (existing?.teamKey) {
+ byTeamKey.delete(existing.teamKey);
+ }
byTeamKey.set(teamKey, snapshot);
}🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/web/src/session-logic.ts around lines 943-945:
When a `runId`'s `teamKey` changes from "A" to "B" across activities, line 944 only sets `byTeamKey.set("B", snapshot)` without removing the stale entry for "A". The old `byTeamKey.get("A")` still returns the outdated snapshot, leaving the index in an inconsistent state where both keys reference different versions of the same run. Consider deleting the old `teamKey` entry when it differs from the current one, or rebuild the index differently.
Evidence trail:
apps/web/src/session-logic.ts lines 830-835 (Map creation and teamKey determination), lines 942-945 (setting entries without deleting old teamKey). The existing?.teamKey is retrieved at line 835 but there's no comparison or deletion of the old key when teamKey changes before lines 942-945.
| return "dynamic_tool_call"; | ||
| } | ||
|
|
||
| function classifyRequestType(toolName: string): CanonicalRequestType { |
There was a problem hiding this comment.
🟢 Low Layers/ClaudeCodeAdapter.ts:416
classifyRequestType returns "file_change_approval" as the fallback for any tool that isn't a read operation or command execution. This incorrectly classifies MCP tools, collab agent tools, and dynamic tool calls as file change approvals. For example, a tool named "mcp_weather" gets classifyToolItemType return of "mcp_tool_call", which is not "command_execution", so classifyRequestType returns "file_change_approval" — semantically incorrect since an MCP weather call is not a file change. Consider adding explicit cases for MCP tools and collab agent tools, or mapping each CanonicalItemType to its corresponding CanonicalRequestType instead of defaulting to file change approval.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/provider/Layers/ClaudeCodeAdapter.ts around line 416:
`classifyRequestType` returns `"file_change_approval"` as the fallback for any tool that isn't a read operation or command execution. This incorrectly classifies MCP tools, collab agent tools, and dynamic tool calls as file change approvals. For example, a tool named `"mcp_weather"` gets `classifyToolItemType` return of `"mcp_tool_call"`, which is not `"command_execution"`, so `classifyRequestType` returns `"file_change_approval"` — semantically incorrect since an MCP weather call is not a file change. Consider adding explicit cases for MCP tools and collab agent tools, or mapping each `CanonicalItemType` to its corresponding `CanonicalRequestType` instead of defaulting to file change approval.
Evidence trail:
apps/server/src/provider/Layers/ClaudeCodeAdapter.ts lines 375-424 (REVIEWED_COMMIT): `classifyToolItemType` returns "mcp_tool_call" for tools containing "mcp" and "collab_agent_tool_call" for team/agent tools. `classifyRequestType` (lines 417-424) only checks for "command_execution" and falls back to "file_change_approval" for all other types, including "mcp_tool_call", "collab_agent_tool_call", and "dynamic_tool_call".
| export const CLAUDE_CLI_PATH = path.join( | ||
| path.dirname(require.resolve("@anthropic-ai/claude-agent-sdk")), | ||
| "cli.js", | ||
| ); |
There was a problem hiding this comment.
🟡 Medium Layers/ProviderHealth.ts:34
CLAUDE_CLI_PATH is evaluated at module load time via require.resolve, so a missing @anthropic-ai/claude-agent-sdk package throws before checkClaudeProviderStatus runs and causes the entire server process to crash. The error handling that reports "Claude Code runtime is not available" is unreachable because the module fails to load first. Consider wrapping the resolution in a try/catch or moving it into checkClaudeProviderStatus so the health check can gracefully report the provider as unavailable.
-export const CLAUDE_CLI_PATH = path.join(
- path.dirname(require.resolve("@anthropic-ai/claude-agent-sdk")),
- "cli.js",
-);
+export let CLAUDE_CLI_PATH: string | undefined;
+try {
+ CLAUDE_CLI_PATH = path.join(
+ path.dirname(require.resolve("@anthropic-ai/claude-agent-sdk")),
+ "cli.js",
+ );
+} catch {
+ CLAUDE_CLI_PATH = undefined;
+}🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/provider/Layers/ProviderHealth.ts around lines 34-37:
`CLAUDE_CLI_PATH` is evaluated at module load time via `require.resolve`, so a missing `@anthropic-ai/claude-agent-sdk` package throws before `checkClaudeProviderStatus` runs and causes the entire server process to crash. The error handling that reports "Claude Code runtime is not available" is unreachable because the module fails to load first. Consider wrapping the resolution in a try/catch or moving it into `checkClaudeProviderStatus` so the health check can gracefully report the provider as unavailable.
Evidence trail:
apps/server/src/provider/Layers/ProviderHealth.ts lines 32-35: `const require = createRequire(import.meta.url); export const CLAUDE_CLI_PATH = path.join(path.dirname(require.resolve("@anthropic-ai/claude-agent-sdk")), "cli.js");` - module-level code that calls require.resolve at load time.
apps/server/src/provider/Layers/ProviderHealth.ts lines 455-466: `checkClaudeProviderStatus` contains the error handling that reports "Claude Code runtime is not available" but this code is unreachable if the module fails to load.
Node.js documentation on require.resolve: throws MODULE_NOT_FOUND if the module cannot be found.
| Effect.result, | ||
| ); | ||
|
|
||
| if (Result.isFailure(authProbe)) { |
There was a problem hiding this comment.
🟠 High Layers/ProviderHealth.ts:619
When the auth probe fails or times out, checkClaudeProviderStatus returns a ServerProviderStatus that omits the capabilities field, while all other return paths include it. Since parsedVersion is already extracted at line 595, the capabilities object can be populated in these branches as well.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/provider/Layers/ProviderHealth.ts around line 619:
When the auth probe fails or times out, `checkClaudeProviderStatus` returns a `ServerProviderStatus` that omits the `capabilities` field, while all other return paths include it. Since `parsedVersion` is already extracted at line 595, the capabilities object can be populated in these branches as well.
Evidence trail:
apps/server/src/provider/Layers/ProviderHealth.ts lines 595, 619-631, 634-643, 562-577, 579-592, 596-612, 645-657 (at REVIEWED_COMMIT). packages/contracts/src/server.ts lines 56-66 for ServerProviderStatus schema definition.
|
we should find a more generic solution to subagent as a whole. this is currently very claude focus and we'd like the frontend to be as agnostic as possible |
|
Can you share some screenshots/videos of how your implementation looks? |
What Changed
Adds comprehensive UI support for Claude Code's experimental Agent Teams feature.
Status Tracking
tool.completed → idlemapping,task.progress/task.completedtracking viateammateTaskIdsendedAt— ended runs show terminal status and zero active countTeam Lifecycle
Panel UI
Settings
Adapter (teams-specific)
in_process_teammatetask type recognition in ingestion pipelineWhy
The agent teams feature was released in Claude Code v2.1.32 but the T3 Code UI had no support for visualizing team activity, tracking agent status, or managing team lifecycle. This PR adds that support, validated against the reverse-engineered Claude Code CLI v2.1.76 event protocol.
UI Changes
Checklist